/**
##############################################################################

DOCOMO Innovations, Inc.

Copyright © 2011 DOCOMO Innovations, Inc. All rights reserved.

 

Date of Preparation: 08/01/2011

Contact: kozat@docomoinnovations.com

 

All intellectual property rights related to this software are vested in DOCOMO Innovations.

DOCOMO Innovations is under no obligation to maintain or support the software.

Neither DOCOMO Innovations nor its employees are responsible for any damage or loss resulting from the use of the software.

##############################################################################
*/

package umich.infinity.proxy;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;

import umich.infinity.thrift.Attr;

/*
 * Manage the caching on local storage.
 * Each file is cached under the CacheRoot as 3 sub files: 
 * the data file (Filename.dat), attr file (Filename.attr), and the meta file (Filename.meta).
 * The data file stores the actual, leaving space if some chunks in between are missing. 
 * The attr file stores file attributes, which are different from 
 * the attributes of the data file
 * The meta file stores the map of available chunk. let b[i] be the ith byte of the meata file. b[i] == 0 means that the ith chunk is not cached, b[i] != 0 means that the ith chunk is cached.
 * 
 *  TODO: this design is used due to its simplicity. It will work fine for small files, 
 *  but will waste local storage space when caching large files.  
 */
public class CacheManager {
	//SharedData sharedData;
	
	public CacheManager() {
		File f =  new File (Para.CacheRoot);
		if (!f.exists()) f.mkdir();
	}
	
	static final String TAG = "InfinityFB.CacheManager";

	/** Initialize the the entries of the a file in the local cache: create the attr and meta files
	 *  but in consideration of performance, the space for the data cache is not allocated 
	    ** 
	    ** @param path the full path of the file in Infinity     
	    ** @param offset offset from the begin of the file, should be multiple of chunk size
	    ** @param size size should be multiple of block size
	    ** @param buf the buf with the actual content
	    ** @return 0 if success. non-zero if error happens.
	    */
	public void initFile(String path, Attr attr) {
		writeFileAttr(path, attr);		
		int numOfChunk = (int) (attr.size / Para.ChunkSize);
		writeFileMeta(path, 0, numOfChunk, (byte) 0);		
	}

	public boolean isUpdated(String path, Attr attr) {
		Attr localAttr = readFileAttr(path);
		
		if (localAttr == null) {
			Util.myDebug(TAG, path + " does not exist!");
			return true;
		} else {
			Util.myDebug(TAG, "L: " + localAttr.modifyTime + " R: " + attr.modifyTime);
			return ((localAttr.modifyTime.compareTo(attr.modifyTime)) < 0);
		}
	}
	
	/** Invalidate the local cache by removing the data, attr, and meta files
	 * 
	 * @param uid the uid of the user
	 * @param path
	 */
	public void invalideCache(String uid, String path) {
		String attrFn = Util.uploadFileAttr(uid, path);
		File f = new File(attrFn);
		f.delete();
		String dataFn = Util.uploadFileData(uid, path);
		File f2 = new File(dataFn);
		f2.delete();		
	}
	
	/** Write data the local cache, and update the attr and meta file cache 
	    ** 
	    * @param uid the uid of the user
	    ** @param path the full path of the file in Infintiy     
	    ** @param offset offset from the begin of the file, should be multiple of chunk size
	    ** @param size size should be multiple of block size
	    ** @param buf the buf with the actual content
	    ** @return 0 if success. non-zero if error happens.
	    */
	public int write(String uid, String path, long offset, long size, ByteBuffer buf) {
		String attrFn =  Util.uploadFileAttr(uid, path);

		File f = new File(attrFn);

		if (!f.exists()) {
			return -1;
		}
		
		Util.myDebug(TAG, path+" "+offset+","+size);
		
		//Write data to the data cache file
		writeFileData(path, offset, size, buf);
		
		//Update the meta file
		int c_offset = (int) (offset/Para.ChunkSize);
		int c_size = (int) (Math.ceil (((double) size) / Para.ChunkSize));
		writeFileMeta(path, c_offset, c_size, (byte) 1);
		
		Attr attr = readFileAttr(path);
		
		//Update the modifyTime
		//attr.modifyTime = Util.getCurTimeString();
		
		long ns = offset+size;
		if (ns > attr.size) {
			attr.size = ns;
		}
		
		writeFileAttr(path, attr);
		
		return 0;
	}
	
	/** Read data from the data file cache. 
	 * 
	 * @param path the full path of the file in Infintiy     
	 * @param offset offset from the begin of the file, should be multiple of chunk size
	 * @param size size should be multiple of block size
	 * @return null if fails, or the data read
	 */
	public ByteBuffer read(String uid, String path, long offset, long size) {
		String attrFn =  Util.uploadFileAttr(uid, path);

		File f = new File(attrFn);

		if (!f.exists() || !isCached(path, offset, size)) {
			Util.myDebug(TAG, "file does not exist");
			return null;
		}
		
		//Write data to the data cache file
		return readFileData(path, offset, size);
	}
	
	/** Write data to the data file cache. 
    ** 
    ** @param path the full path of the file in Infintiy     
    ** @param offset offset from the begin of the file, should be multiple of chunk size
    ** @param size size should be multiple of block size
    ** @param buf the buf with the actual content
    ** @return 0 if success. non-zero if error happens.
    */

	public void writeFileData(String path, long offset, long size, ByteBuffer buf) {
		String fn = Util.uploadFileData(path);
		
		File f = new File(fn);

		try {
			RandomAccessFile raf = new RandomAccessFile(f, "rw");

			// Extend the original cache file if it is too small
			if (raf.length() < (offset + size)) {
				raf.setLength(offset + size);
			}

			raf.seek(offset);

			if (buf!=null) {
				raf.write(buf.array(), 0, (int) size);
			}

			raf.close();
		} catch (IOException x) {
			Util.myDebug(TAG, x.toString());
		}
	}

	/** Read data from the data file cache. 
	 * 
	 * @param path the full path of the file in Infintiy     
	 * @param offset offset from the begin of the file, should be multiple of chunk size
	 * @param size size should be multiple of block size
	 * @return null if fails, or the data read
	 */

	public ByteBuffer readFileData(String path, long offset, long size) {
		String fn = Util.uploadFileData(path);

		File f = new File(fn);

		try {
			RandomAccessFile raf = new RandomAccessFile(f, "r");

			raf.seek(offset);

			ByteArrayOutputStream out = new ByteArrayOutputStream(Para.ChunkSize);
	    	byte[] tmp = new byte[Para.ChunkSize];

	    	long readSize= 0 ;
	    	while (readSize < size) {
	    	    int r = raf.read(tmp, 0, (int) Para.ChunkSize);
	    	    if (r == -1) break;
	    	    
	    	    out.write(tmp,0,r);
	    	    readSize += r;
	    	}
	    	
	    	raf.close();
			
			return ByteBuffer.wrap(out.toByteArray());
		} catch (IOException x) {
			Util.myDebug(TAG, x.toString());
		}

		return null;
	}
	
	/**
	 * Read the next counter for the user, and increase the counter in the counter file
	 * @param uid
	 * @return the counter for the upload
	 */
	public synchronized long getNextCounter(String uid) {
		File d = new File (Para.CacheRoot + uid);
		if (!d.exists()) 
			d.mkdir();
		
		String fn = Util.uploadFileCounter(uid);

		File f = new File (fn);

		RandomAccessFile raf;
		long ret = 0;

		if (f.exists()) {
			try {
				raf = new RandomAccessFile(f, "r");
				ret = raf.readLong();

				raf.close();

				f.delete();
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		try{
			raf = new RandomAccessFile (f, "rw");

			raf.writeLong(ret+1);
			raf.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return ret;
	}
	
	/**
	 ** Create the attr file and populate with the attributes in attr
	 ** 
	 **  @param path the full path of the file in Infinity
	 **  @param attr the initial attr to be written
	 **/
	public void writeFileAttr(String path, Attr attr) {
		String attrFn = Util.uploadFileAttr(path);
		
		File f = new File(attrFn);
		try {
			FileOutputStream fos = new FileOutputStream(f);
			ObjectOutputStream oos = new ObjectOutputStream(fos);

			oos.writeObject(attr);

			oos.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}			
	}
	
	/**
	 ** Read the attributes from the file
	 ** 
	 **  @param path the full path of the file in Infinity
	 **  @param attr the initial attr to be written
	 ** 
	 */
	public Attr readFileAttr(String path) {
		String attrFn = Util.uploadFileAttr(path);
		
		File f = new File(attrFn);
		
		if (!f.exists()) {
			return null;
		} 
		
		try {
			FileInputStream fis = new FileInputStream(f);
			ObjectInputStream ois = new ObjectInputStream(fis);

			Attr attr = (Attr) ois.readObject();

			ois.close();

			return attr;
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return null;
	}
	
	/** 
	 **  Write the meta file, update the (offset, offset+numOfChunk) bytes to value
	 ** 
	 **  @param path the full path of the file in Infinity
	 **  @param offset the offset of bytes from the beginning of the file
	 **  @param size the number of bytes (chunks) to update
	 **  @param value the value (0 or non-zero) to write 
	 **/
	public void writeFileMeta(String path, int offset, int size, byte value) {
		String metaFn = Util.uploadFileMeta(path);
		
		File f = new File(metaFn);
		
		try {
			RandomAccessFile raf = new RandomAccessFile(f, "rw");

			// Extend the original cache file if it is too small
			if (raf.length() < (offset + size)) {
				raf.setLength(offset + size);
			}

			raf.seek(offset);

			byte[] buf = new byte[size];
			for (int i=0;i<size;i++) {
				buf[i] = value;
			}
			raf.write(buf, 0, (int) size);

			raf.close();
		} catch (IOException x) {
			Util.myDebug(TAG, x.toString());
		}	
	}
	
	/** 
	 **  Read the meta file from (offset, offset+size) 
	 ** 
	 **  @param path the full path of the file in Infinity
	 **  @param offset the offset of bytes from the beginning of the meta file
	 **  @param size the number of bytes (chunks) to update
	 **  @return the byte array read from the file	 
	 **/
	public byte[] readFileMeta(String path, int offset, int size) {
		String metaFn = Util.uploadFileMeta(path);
		
		File f = new File(metaFn);
		
		if (!f.exists()) {
			return null;
		} 
		
		try {
			RandomAccessFile raf = new RandomAccessFile(f, "rw");

			// Extend the original cache file if it is too small
			if (raf.length() < (offset + size)) {
				long ol = raf.length();
				raf.setLength(offset + size);
				raf.seek(ol-1);
				
				long diff = offset+size - ol;
				byte[] buf = new byte[(int) diff];
				for (int i=0;i<diff;i++) {
					buf[i] = 0;
				}
				raf.write(buf, 0, (int) diff);				
			}
			
			raf.seek(offset);
			byte[] res = new byte[size];
			
		    raf.read(res, 0, size);
		    
		    return res;
		} catch (IOException x) {
			Util.myDebug(TAG, x.toString());
		}

		return null;
	}
	
	
	
	/** 
	 **  check whether the chunks (offset, offset+size) are all cached 
	 ** 
	 **  @param path the full path of the file in Infinity
	 **  @param offset the offset of bytes from the beginning of the file
	 **  @param size the number of bytes (chunks) to update
	 **  @return true if all the chunks (offset, offset+size) are cached locally	 
	 **/
	public boolean isCached(String path, long offset, long size) {
		int c_offset = (int) (Math.floor(((double) offset) / Para.ChunkSize));
		int c_size = (int) (Math.ceil (((double) size) / Para.ChunkSize));
		
		byte[] buf = readFileMeta(path, c_offset, c_size);
		
		if (buf == null) {
			return false;
		}
		
		for (int i=0;i<buf.length;i++) {
			if (buf[i] == 0) {
				return false;
			}
		}
		
		return true;
	}
	
	/** 
	 **  check whether all the chunks are cached 
	 ** 
	 **  @param path the full path of the file in Infinity
	 **  @return true if all the chunks are cached locally	 
	 **/
	public boolean isCached(String path) {
		Attr attr = readFileAttr(path);
		if (attr == null) {
			return false;
		}
		
		return isCached(path, 0, attr.size);		
	}	
}
